Bitmap
Android 之 Bitmap
Bitmap 介绍
Bitmap 代表一张位图,位图文件图像效果好(高质量图片格式),但是非压缩格式的,需要占用较大存储空间,不利于网络上传送。而 JPEG 格式就弥补了位图文件这个缺点。
在 Android 中,Bitmap 是图像处理最重要的类之一,用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。
Bitmap 获取
通过 BitmapFactory 获取
- 通过资源 id decodeResource()
- 通过文件路径
- 通过字节数组
- 通过数据流 decodeStream()
BitmapFactory.Options
- inJustDecodeBounds
设置为 true 后,不会真正分配 Bitmap 所占用的内存空间,仅仅获取一些属性 - inSampleSize
缩放图片采用的比率值 - inPreferredConfig = Bitmap.Config.ARGB_8888
设置图片的色彩模式,默认 ARGB_8888。可选见 Bitmap.Config:ALPHA_8、RGB_565、ARGB_4444(过时)、默认 ARGB_8888。
Bitmap 图片处理
通过 Bitmap 对图片的操作,都是通过 jni 来实现,调用 skia 这个库(具体可以操作 bugly 的 bitmap 占用内存那篇文件)。
- 剪切
Bitmap.createBitmap() - 缩放
Matrix.postScale() - 旋转
Matrix.postRotate() - 平移
Matrix.postTranslate() - 保存
compress()
图片到底储存在哪里?
8.0Bitmap 的像素数据存储在 Native,为什么又改为 Native 存储呢?
因为 8.0 共享了整个系统的内存,测试 8.0 手机如果一直创建 Bitmap,如果手机内存有 1G,那么你的应用加载 1G 也不会 oom。
Bitmap 分块加载(加载巨图之图片)
加载清明上河图,要求我们既不能压缩图片,又不能发生 oom 怎么办?
图片分块加载
图片的分块加载,在地图绘制的情况上最为明显,当想要获取一张尺寸很大的图片的某一小块区域时,就可以用到了图片的分块加载。
如显示:世界地图、清明上河图、微博长图等。
BitmapRegionDecoder
BitmapRegionDecoder 用来解码图片中的一块矩形区域,典型用法是加载一张大图的小部分。
//支持传入图片的路径,流和图片修饰符等
BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//需要显示的区域就有由rect控制,options来控制图片的属性
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
由于要显示一部分区域,所以要有手势的控制,方便上下的滑动,需要自定义控件,而自定义控件的思路也很简单 1 提供图片的入口 2 重写 onTouchEvent, 根据手势的移动更新显示区域的参数 3 更新区域参数后,刷新控件重新绘制。
public class BigImageView extends View {
private BitmapRegionDecoder mDecoder;
private int mImageWidth;
private int mImageHeight;
//图片绘制的区域
private Rect mRect = new Rect();
private static final BitmapFactory.Options options = new BitmapFactory.Options();
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
public BigImageView(Context context) {
super(context);
init();
}
public BigImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BigImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
/**
* 自定义view的入口,设置图片流
*
* @param path 图片路径
*/
public void setFilePath(String path) {
try {
//初始化BitmapRegionDecoder
mDecoder = BitmapRegionDecoder.newInstance(path, false);
BitmapFactory.Options options = new BitmapFactory.Options();
//便是只加载图片属性,不加载bitmap进入内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
//图片的宽高
mImageWidth = options.outWidth;
mImageHeight = options.outHeight;
Log.d("mmm", "图片宽=" + mImageWidth + "图片高=" + mImageHeight);
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取本view的宽高
int measuredHeight = getMeasuredHeight();
int measuredWidth = getMeasuredWidth();
//默认显示图片左上方
mRect.left = 0;
mRect.top = 0;
mRect.right = mRect.left + measuredWidth;
mRect.bottom = mRect.top + measuredHeight;
}
//第一次按下的位置
private float mDownX;
private float mDownY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
//移动的距离
int xDistance = (int) (moveX - mDownX);
int yDistance = (int) (moveY - mDownY);
Log.d("mmm", "mDownX=" + mDownX + "mDownY=" + mDownY);
Log.d("mmm", "movex=" + moveX + "movey=" + moveY);
Log.d("mmm", "xDistance=" + xDistance + "yDistance=" + yDistance);
Log.d("mmm", "mImageWidth=" + mImageWidth + "mImageHeight=" + mImageHeight);
Log.d("mmm", "getWidth=" + getWidth() + "getHeight=" + getHeight());
if (mImageWidth > getWidth()) {
mRect.offset(-xDistance, 0);
checkWidth();
//刷新页面
invalidate();
Log.d("mmm", "刷新宽度");
}
if (mImageHeight > getHeight()) {
mRect.offset(0, -yDistance);
checkHeight();
invalidate();
Log.d("mmm", "刷新高度");
}
break;
case MotionEvent.ACTION_UP:
break;
default:
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bitmap, 0, 0, null);
}
/**
* 确保图不划出屏幕
*/
private void checkWidth() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.right > imageWidth) {
rect.right = imageWidth;
rect.left = imageWidth - getWidth();
}
if (rect.left < 0) {
rect.left = 0;
rect.right = getWidth();
}
}
/**
* 确保图不划出屏幕
*/
private void checkHeight() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight;
rect.top = imageHeight - getHeight();
}
if (rect.top < 0) {
rect.top = 0;
rect.bottom = getHeight();
}
}
}
Reference
Bitmap 内存
Bitmap 内存如何计算?
占用内存 = (图片宽度/inSampleSize X inTargetDensity/inDensity) X (图片高度/inSampleSize X inTargetDensity/inDensity) X 每个像素所占的内存
通俗点讲就是:内存占用 = 宽 X高 X 每个像素所占的内存
inSampleSize
inSampleSize 表示采样率,为 2 的整数次幂。
设置了 inSampleSize 图片的宽高对应的缩小 inSampleSize 的倍数,如 inSampleSize=2,缩小 4 倍
dpi
Bitmap.Config

Bitmap 到底占多大内存
本地磁盘/网络加载图片
从本地加载或者从网络加载可以用下面的公式计算:
图片的长度 * 图片的宽度 * 一个像素点占用的字节数
一张图片在不同 ImageView 宽高内存占用?
一样
本地 drawable 资源文件加载图片
如果从本地资源文件夹加载
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (当前设备密度dpi/图片所在文件夹对应的密度dpi)^2 × 每个像素的字节大小
同一张图片放进不同的文件夹,图片会被压缩。看下源码:
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
// ...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
// ...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
压缩比例是由下面的公式得出:
scale = (float) targetDensity / density;
- targetDensity:设备屏幕像素密度 dpi
- density:图片对应的文件夹的像素密度 dpi,其中 density 和 Bitmap 存放的资源目录有关,不同的资源目录有不同的值

可以得出以下结论:
- 同一张图片放在不同的资源目录下,其分辨率会有变化。
- Bitmap 的分辨率越高,其解析后的宽高越小,甚至小于原有的图片(及缩放),从而内存也响应的减少。
- 图片不放置任何资源目录时,其使用默认分辨率 mdpi:160。
- 资源目录分辨率和屏幕分辨率一致时,图片尺寸不会缩放。
Bitmap 内存优化
Bitmap 内存优化从下面四个方面进行优化:
- 编码
- 采样
- 复用
- 匿名共享区
编码 (优化单位像素占用内存)
Android 中提供以下几种编码:
- ALPHA_8 表示 8 位 Alpha 位图,即 A=8,一个像素点占用 1 个字节,它没有颜色,只有透明度。
- ARGB_4444 表示 16 位 ARGB 位图,即 A=4,R=4,G=4,B=4,一个像素点占 4+4+4+4=16 位,2 个字节。
- ARGB_8888 表示 32 位 ARGB 位图,即 A=8,R=8,G=8,B=8,一个像素点占 8+8+8+8=32 位,4 个字节。
- RGB_565 表示 16 位 RGB 位图,即 R=5,G=6,B=5,它没有透明度,一个像素点占 5+6+5=16 位,2 个字节。
A 代表透明度;R 代表红色;G 代表绿色;B 代表蓝色。
可以通过改变图片格式,来改变每个像素占用字节数,来改变占用的内存,看下面代码:
BitmapFactory.Options options = new BitmapFactory.Options();
//不获取图片,不加载到内存中,只返回图片属性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//图片的宽高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "图片宽=" + outWidth + "图片高=" + outHeight);
//图片格式压缩
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","压缩后:图片占内存大小" + bitmapsize + "MB / 宽度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
输出:
D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
D/mmm: 图片宽=4000图片高=3000
D/mmm: 压缩后:图片占内存大小22.887695MB / 宽度=4000高度=3000
宽高没变,我们改变了图片的格式,从 ARGB_8888 变成了 RGB_565,像素占用字节数减少了一般,根据 log 内存也减少了一半,这种方式可行
采样 inSampleSize (优化 Bitmap 的加载时的宽高)
通过采样,不加载 bitmap 真实的宽高,通过 inSampleSize 只采样实际控件需要用到的宽高
注意:inSampleSize=2,缩放 1/4,长和宽各缩放 1/2
BitmapFactory.Options options = new BitmapFactory.Options();
//不获取图片,不加载到内存中,只返回图片属性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//图片的宽高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "图片宽=" + outWidth + "图片高=" + outHeight);
//计算采样率
int i = utils.computeSampleSize(options, -1, 1000 * 1000);
//设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推
options.inSampleSize = i;
Log.d("mmm", "采样率为=" + i);
//图片格式压缩
//options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","压缩后:图片占内存大小" + bitmapsize + "MB / 宽度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
输出:
D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
D/mmm: 图片宽=4000图片高=3000
D/mmm: 采样率为=4
D/mmm: 压缩后:图片占内存大小1.4296875MB / 宽度=1000高度=750
这种我们根据 BitmapFactory 的采样率进行压缩 设置采样率,不能小于 1 假如是 2 则宽为之前的 1/2,高为之前的 1/2,一共缩小 1/4 一次类推,我们看到 log ,确实起到了压缩的目的
复用 inBitmap
图片复用指的是 inBitmap 这个属性。
- 不使用 inBitmap
不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片;如果用了 inBitmap 这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间。
- inBitmap 的限制
- 3.0-4.3 复用的图片大小必须相同;编码必须相同
- 4.4 以上 复用的空间大于等于即可; 编码不必相同
- 不支持 WebP
- 图片复用,这个属性必须设置为 true;options.inMutable = true;
匿名共享区(Ashmem)
Android 系统为了进程间共享数据开辟的一块内存区域,由于这块区域不受应用的 Head 的大小限制,相当于可以绕开 oom,FaceBook 的 Fresco 首次应用到实际中。
限制:5.0 以后就限制了匿名共享内存的使用。
Bitmap 优化手段
LRU 管理 Bitmap
利用 LRU 开管理 Bitmap,给他设置内存最大值,及时回收
Bitmap Pool
Bitmap 对象池
图片的压缩
- 采样压缩,节省内存
- 质量压缩,节省空间,不会改变图片在内存中的大
bitmap.compress(Bitmap.CompressFormat.JPEG, 20,
new FileOutputStream("sdcard/result.jpg"));